/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;

/**
 * Used to enhance performance. Different startup extensions need not check node type creation/removal each for its own.
 * 
 * @author Walter Luetgenau
 */
public class NodeTypeStartupExtension implements RepositoryStartupExtension, EventListener {

    static NodeTypeStartupExtension instance;
    HashMap<String, NodeTypeObserver> observers = new HashMap<String, NodeTypeObserver>();
    List<String> observedNodeTypes = new ArrayList<String>();
    Session systemSession;

    /**
     * Singleton accessor
     * 
     * @return
     */
    public static synchronized NodeTypeStartupExtension getInstance() {
        if (instance == null)
            instance = new NodeTypeStartupExtension();
        return instance;
    }

    /**
     * Add an observer for a node type
     * 
     * @param nodeType
     * @param observer
     */
    public void addNodeTypeObserver(String nodeType, NodeTypeObserver observer) throws RepositoryException {
        observers.put(nodeType, observer);
        observedNodeTypes.add(nodeType);

        initObserver(nodeType, observer);
    }

    /**
     * Init the observer, i.e. notify it about all nodes of the given type currently present in the repository
     *
     * @param nodeType the node type requested
     * @param observer the observer, that will be notified
     * @throws RepositoryException
     */
    protected void initObserver(String nodeType, NodeTypeObserver observer) throws RepositoryException {
        XPathQuery query = new XPathQuery(systemSession);
        String queryString = "/jcr:root//element(*, " + nodeType + ")";


        NodeIterator ni = query.executeQuery(queryString);
        Node[] result = new Node[(int) ni.getSize()];
        for (int i = 0; ni.hasNext(); i++) {
            observer.nodeAdded(ni.nextNode());
        }
    }

    /**
     * Register an event listener for creation and modification stamps.
     * Called by the RepositoryExtensionsManager on startup.
     * 
     * @param systemSession
     */
    public void onStartup(Session systemSession) {
        try {
            this.systemSession = systemSession;

            // add node type observer
            systemSession.getWorkspace().getObservationManager().addEventListener(
                    this,
                    Event.NODE_ADDED | Event.NODE_REMOVED, "/",
                    true,
                    null,
                    null,
                    true);

        } catch (RepositoryException ex) {
            Logger.getLogger(NodeTypeStartupExtension.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * On an NODE_ADDED or NODE_REMOVED event, check, if this was the nodeType in question. If true, notify observers
     *
     * @param events
     */
    public void onEvent(EventIterator events) {
        try {
            Node root = systemSession.getRootNode();

            while (events.hasNext()) {
                Event event = events.nextEvent();

                String path = event.getPath().substring(1);
 
                if (event.getType() == Event.NODE_REMOVED) {
                    notifyNodeRemoved(event.getPath());
                    continue;
                }

                if (!root.hasNode(path))
                    continue;

                Node node = root.getNode(path);
                notifyNodeAdded(node);
            }

        } catch (Throwable ex) {
            Logger.getLogger(NodeTypeStartupExtension.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * If required, notify observers about the new node of node type
     *
     * @param node
     * @throws RepositoryException
     */
    protected void notifyNodeAdded(Node node) throws RepositoryException {
        for (String nodeType : observedNodeTypes)
            if (node.isNodeType(nodeType)) {
                observers.get(nodeType).nodeAdded(node);
                break;
            }
    }

    /**
     * If required, notify all observers about the removed node,
     * since we do not know its type
     *
     * @param path the path to the node, that has been removed
     * @throws RepositoryException
     */
    protected void notifyNodeRemoved(String path) throws RepositoryException {
        for (NodeTypeObserver observer : observers.values())
            observer.nodeRemoved(path);
    }
}
